package asteroids.model;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.List;
import java.lang.StringBuilder;

import org.antlr.v4.runtime.RecognitionException;

import asteroids.*;
import asteroids.expression.*;
import asteroids.statement.*;
import asteroids.model.programs.*;
import asteroids.model.programs.parsing.*;

public class Facade implements IFacade<World, Ship, Asteroid, Bullet, Program> {
	@Override
	public World createWorld(double width, double height) throws ModelException {
		try {
			World world = new World(width, height);
			return world;
		} catch (IllegalArgumentException e) {
			throw new ModelException(
					"please provide numbers as parameters for the world");
		}

	}

	@Override
	public double getWorldWidth(World world) {
		return world.getWorldWidth();
	}

	@Override
	public double getWorldHeight(World world) {

		return world.getWorldHeight();
	}

	@Override
	public Set<Ship> getShips(World world) {
		Set<Ship> shipSet = new HashSet<Ship>();
		shipSet.addAll(world.getShips());
		return shipSet;
	}

	@Override
	public Set<Asteroid> getAsteroids(World world) {
		Set<Asteroid> asteroidSet = new HashSet<Asteroid>();
		asteroidSet.addAll(world.getAsteroids());
		return asteroidSet;
	}

	@Override
	public Set<Bullet> getBullets(World world) {
		Set<Bullet> bulletSet = new HashSet<Bullet>();
		bulletSet.addAll(world.getBullets());
		return bulletSet;
	}

	@Override
	public void addShip(World world, Ship ship) throws ModelException {
		try { world.addShip(ship); }
		catch(IllegalArgumentException exc) {
			throw new ModelException("Ship is not within bounds or is already within this world.");
		}

	}

	@Override
	public void addAsteroid(World world, Asteroid asteroid) throws ModelException {
		try { world.addAsteroid(asteroid); }
		catch(IllegalArgumentException exc) {
			throw new ModelException("Asteroid is not within bounds or is already within this world.");
		}

	}

	@Override
	public void removeShip(World world, Ship ship) throws ModelException {
		if(world == null)
			throw new ModelException("Please provide a world.");
		world.destroyFlyingObject(ship);

	}

	@Override
	public void removeAsteroid(World world, Asteroid asteroid) throws ModelException {
		if(world == null)
			throw new ModelException("Please provide a world.");
		world.destroyFlyingObject(asteroid);
	}

	@Override
	public void evolve(World world, double dt,
			CollisionListener collisionListener) throws ModelException {
		if(world == null || collisionListener == null)
			throw new ModelException("Please provide a World and a CollisionListener");
		world.setCollisionListener(collisionListener);
		world.evolve(dt);

	}

	@Override
	public Ship createShip(double x, double y, double xVelocity,
			double yVelocity, double radius, double direction, double mass)
			throws ModelException {
		try {
			Position pos = new Position(x, y);
			Velocity speed = new Velocity(xVelocity, yVelocity);
			Angle ang = new Angle(direction);
			Ship ship = new Ship(pos, speed, ang, radius, mass);
			setThrusterActive(ship, false);
			return ship;
		} catch (IllegalArgumentException e) {
			throw new ModelException(
					"Please provide numbers as the parameters for the ship");
		} catch (NullPointerException e) {
			throw new ModelException("Internal error.");
		}

	}

	@Override
	public boolean isShip(Object o) {
		return o instanceof Ship;
	}

	@Override
	public double getShipX(Ship ship) throws ModelException {
		if (ship == null) {
			throw new ModelException("Please provide a valid Ship.");
		} else {
			return ship.getPosition().getX();
		}
	}

	@Override
	public double getShipY(Ship ship) throws ModelException {

		if (ship == null) {
			throw new ModelException("Please provide a valid Ship.");
		} else {
			return ship.getPosition().getY();
		}
	}

	@Override
	public double getShipXVelocity(Ship ship) {
		if (ship == null) {
			return 0;
		} else {
			return ship.getVelocity().getX();
		}
	}

	@Override
	public double getShipYVelocity(Ship ship) {
		if (ship == null) {
			return 0;
		} else {
			return ship.getVelocity().getY();
		}
	}

	@Override
	public double getShipRadius(Ship ship) throws ModelException {
		if (ship == null) {
			throw new ModelException("Please provide a valid ship.");
		}
		return ship.getSize();
	}

	@Override
	public double getShipDirection(Ship ship) {
		assert (ship != null);
		return ship.getAngle().getAngle();
	}

	@Override
	public double getShipMass(Ship ship) throws ModelException {
		if(ship == null)
			throw new ModelException("Please provide a Ship.");
		return ship.getMass();
	}

	@Override
	public World getShipWorld(Ship ship) throws ModelException {
		if(ship == null)
			throw new ModelException("Please provide a Ship.");
		return ship.getWorld();
	}

	@Override
	public boolean isShipThrusterActive(Ship ship) {
		return ship.getThrusterStatus();
	}

	@Override
	public void setThrusterActive(Ship ship, boolean active) {
		ship.setThrusterStatus(active);
	}

	// nominal
	@Override
	public void turn(Ship ship, double angle) {
		assert (ship != null);
		ship.turn(angle);

	}

	@Override
	public void fireBullet(Ship ship) throws ModelException {
		if(ship == null)
			throw new ModelException("Please provide a ship.");
		ship.getWorld().fireBullet(ship);
	}

	@Override
	public Asteroid createAsteroid(double x, double y, double xVelocity,
			double yVelocity, double radius) throws ModelException {
		try {
			Position pos = new Position(x, y);
			Velocity speed = new Velocity(xVelocity, yVelocity);
			Asteroid asteroid = new Asteroid(pos, speed, radius, 0);
			return asteroid;
		} catch (IllegalArgumentException e) {
			throw new ModelException(
					"Please provide numbers as the parameters for the asteroid");
		} catch (NullPointerException e) {
			throw new ModelException("Internal error.");
		}
	}

	@Override
	public Asteroid createAsteroid(double x, double y, double xVelocity,
			double yVelocity, double radius, Random random)
			throws ModelException {
		try {
			Position pos = new Position(x, y);
			Velocity speed = new Velocity(xVelocity, yVelocity);
			Asteroid asteroid = new Asteroid(pos, speed, radius,
					random.nextDouble() * 2.0 * Math.PI);
			return asteroid;
		} catch (IllegalArgumentException e) {
			throw new ModelException(
					"Please provide numbers as the parameters for the asteroid");
		} catch (NullPointerException e) {
			throw new ModelException("Internal error.");
		}
	}

	@Override
	public boolean isAsteroid(Object o) throws ModelException {
		if( o == null)
			throw new ModelException("Please provide an object.");
		return o instanceof Asteroid;
	}

	@Override
	public double getAsteroidX(Asteroid asteroid) throws ModelException {
		if (asteroid == null) {
			throw new ModelException("Please provide a valid Asteroid.");
		} else {
			return asteroid.getPosition().getX();
		}

	}

	@Override
	public double getAsteroidY(Asteroid asteroid) throws ModelException {
		if (asteroid == null) {
			throw new ModelException("Please provide a valid Asteroid.");
		} else {
			return asteroid.getPosition().getY();
		}
	}

	@Override
	public double getAsteroidXVelocity(Asteroid asteroid) {
		if (asteroid == null) {
			return 0;
		} else {
			return asteroid.getVelocity().getX();
		}
	}

	@Override
	public double getAsteroidYVelocity(Asteroid asteroid) {
		if (asteroid == null) {
			return 0;
		} else {
			return asteroid.getVelocity().getY();
		}
	}

	@Override
	public double getAsteroidRadius(Asteroid asteroid) {
		if (asteroid == null) {
			throw new ModelException("Please provide a valid asteroid.");
		}
		return asteroid.getSize();
	}

	@Override
	public double getAsteroidMass(Asteroid asteroid) throws ModelException {
		if(asteroid == null)
			throw new ModelException("Please provide an asteroid.");
		return asteroid.getMass();
	}

	@Override
	public World getAsteroidWorld(Asteroid asteroid) throws ModelException {
		if(asteroid == null)
			throw new ModelException("Please provide an asteroid.");
		return asteroid.getWorld();
	}

	@Override
	public boolean isBullets(Object o) throws ModelException {
		if(o == null)
			throw new ModelException("Please provide an object.");
		return o instanceof Bullet;
	}

	@Override
	public double getBulletX(Bullet bullet) {
		if (bullet == null) {
			return 0;
		} else {
			return bullet.getPosition().getX();
		}
	}

	@Override
	public double getBulletY(Bullet bullet) {
		if (bullet == null) {
			return 0;
		} else {
			return bullet.getPosition().getY();
		}
	}

	@Override
	public double getBulletXVelocity(Bullet bullet) {
		if (bullet == null) {
			return 0;
		} else {
			return bullet.getVelocity().getX();
		}
	}

	@Override
	public double getBulletYVelocity(Bullet bullet) {
		if (bullet == null) {
			return 0;
		} else {
			return bullet.getVelocity().getY();
		}
	}

	@Override
	public double getBulletRadius(Bullet bullet) throws ModelException {
		if (bullet == null) {
			throw new ModelException("Please provide a valid bullet.");
		}
		return bullet.getSize();
	}

	@Override
	public double getBulletMass(Bullet bullet) throws ModelException {
		if(bullet == null)
			throw new ModelException("Please provide a bullet.");
		return bullet.getMass();
	}

	@Override
	public World getBulletWorld(Bullet bullet) throws ModelException{
		if(bullet == null)
			throw new ModelException("Please provide a bullet.");
		return bullet.getWorld();
	}

	@Override
	public Ship getBulletSource(Bullet bullet) throws ModelException {
		if(bullet == null)
			throw new ModelException("Please provide a bullet.");
		return bullet.getSource();
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> parseProgram(String text) {
		ProgramFactoryImpl<Expression,Statement,Expression> factory = new ProgramFactoryImpl<Expression,Statement,Expression>();
		ProgramParser<Expression, Statement, Expression> parser = new ProgramParser<Expression, Statement, Expression>(factory);
		try {
			parser.parse(text);
			List<String> errors = parser.getErrors();
			if (!errors.isEmpty()) {
				return ParseOutcome.failure(errors.get(0));
			} else {
				return ParseOutcome.success(new Program(parser.getGlobals(),
						parser.getStatement()));
			}
		} catch (RecognitionException e) {
			return ParseOutcome.failure(e.getMessage());
		}
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromStream(
			InputStream stream) throws IOException {
		char[] buffer = new char[5];
		StringBuilder builder = new StringBuilder();
		InputStreamReader reader = new InputStreamReader(stream);
		int i = reader.read(buffer, 0,buffer.length);
		
		while (i > 0) {
			builder.append(buffer, 0, i);
			i = reader.read(buffer, 0,buffer.length);
		}
		reader.close();
		System.out.println(builder.toString());
		return parseProgram(builder.toString());
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromUrl(URL url)
			throws IOException, ModelException {
		if(url == null)
			throw new ModelException("Please provide a url.");
		InputStream stream = url.openStream();
		return loadProgramFromStream(stream);
	}

	@Override
	public boolean isTypeCheckingSupported() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public asteroids.IFacade.TypeCheckOutcome typeCheckProgram(Program program) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void setShipProgram(Ship ship, Program program) {
		ship.loadProgram(program);

	}

}
